package meta.symbolsAndTime.SAUNA;

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Vector;

import javax.imageio.ImageIO;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;

import org.metacsp.meta.symbolsAndTime.Schedulable;

import multi.SAUNA.Polyhedron;
import multi.SAUNA.Line;
import multi.SAUNA.Path;
import multi.SAUNA.TemporalPolyhedron;

import org.metacsp.multi.activity.Activity;
import org.metacsp.multi.allenInterval.AllenIntervalConstraint;
import org.metacsp.multi.spatial.rectangleAlgebra.Point;
import org.metacsp.time.Bounds;

import utility.MapUpdater;

import org.metacsp.framework.Constraint;
import org.metacsp.framework.ConstraintNetwork;
import org.metacsp.framework.ConstraintSolver;
import org.metacsp.framework.ValueOrderingH;
import org.metacsp.framework.Variable;
import org.metacsp.framework.VariableOrderingH;

import cern.colt.Arrays;

public class Map extends Schedulable {
	
	/**
	 * 
	 */
	private static final long serialVersionUID = -5661742010654523320L;
	//meters per value in map
	public static double SCALE = 0.2;
	//pixels per meter
	public static final int RESOLUTION = 8;
	//when is the scan line an obstacle?
	public static final double OBSTACLETHRESHOLD = 0.5;
	//should I write an image file when solved?
	public static final boolean writeImage = false;
	
	
	//the actual map
	double[][] map;
	
	//paths on the map
	private HashMap<String,Path> paths;
	
	//Animator thread
	private MapUpdater animator;
	
	//Drawing stuff
	private JPanel myPanel;
	private Point[] decorationPoints;
	private final JButton animateButton;
	private final JButton solveButton;
	private final JButton resetButton;
	private Vector<AllenIntervalConstraint> toAddOnlineConstraints;
	private Vector<AllenIntervalConstraint> onlineConstraints;
	private Vector<AllenIntervalConstraint> offlineConstraints;
	
	public Map (String filename, VariableOrderingH varOH, ValueOrderingH valOH) {
		super(varOH, valOH);
		this.peakCollectionStrategy = PEAKCOLLECTION.COMPLETE;
		try {
			FileInputStream fis = new FileInputStream(filename);
			BufferedReader br = new BufferedReader(new InputStreamReader(fis));
			int width = 0, height = 0, yCounter = 0;
			boolean mapInit = false;
			String line;
			while ((line = br.readLine()) != null) {
				if (line.startsWith("#")) {
					String[] elements = line.split("\\s+");
					for (int i = 0; i < elements.length; i++) {
						if (elements[i].equals("GRID_SIZE")) {
							SCALE = new Double(elements[i+1]);
						}
						else if (elements[i].equals("WIDTH")) {
							width = new Integer(elements[i+1]);
						}
						else if (elements[i].equals("HEIGHT")) {
							height = new Integer(elements[i+1]);
						}
					}
				}
				else {
					if (!mapInit) {
						map = new double[width][height];
						mapInit = true;
					}
					String[] elements = line.split("\\s+");
					for (int i = 0; i < width; i++) {
						//System.out.println("(" + i + "," + (height-yCounter) + ")");
						map[i][height-yCounter-1] = new Double(elements[i]).doubleValue();
					}
					yCounter++;
				}
			}					
			//System.out.println("Loaded map " + width + "x" + height + " (resolution: " + SCALE + "m)");
			paths = new HashMap<String, Path>();
			br.close();
		}
		catch (FileNotFoundException e) { e.printStackTrace(); }
		catch (IOException e) { e.printStackTrace(); }
		animateButton = new JButton("Simulate");
		animateButton.setEnabled(false);
		solveButton = new JButton("Solve");
		solveButton.setEnabled(true);
		resetButton = new JButton("Reset");
		resetButton.setEnabled(false);
		toAddOnlineConstraints = new Vector<AllenIntervalConstraint>();
		onlineConstraints = new Vector<AllenIntervalConstraint>();
		offlineConstraints = new Vector<AllenIntervalConstraint>();
	}
	
	public Point toScreen(double x, double y) {
		return new Point(x*RESOLUTION, ((map[0].length-1)*SCALE-y)*RESOLUTION);
	}

	public Point toMap(int x, int y) {
		return new Point((double)x/RESOLUTION, ((map[0].length-1)*SCALE-((double)y/RESOLUTION)));
	}

	public boolean isObstacle(Point p) {
		return (map[new Double(p.x/SCALE).intValue()][new Double(p.y/SCALE).intValue()] >= OBSTACLETHRESHOLD);
	}
	
	@Override
	public void draw(ConstraintNetwork network) {
		Variable[] vars = network.getVariables();
		final Polyhedron[] cps = new Polyhedron[vars.length];
		final TemporalPolyhedron[] cpsVar = new TemporalPolyhedron[vars.length];
		final String[] cpNames = new String[vars.length];
		for (int i = 0; i < vars.length; i++) {
			cps[i] = ((TemporalPolyhedron)vars[i]).getSpatialPolyhedron();
			cpNames[i] = "" + ((TemporalPolyhedron)vars[i]).getID();
			cpsVar[i] = ((TemporalPolyhedron)vars[i]);
		}
		final int xSize = (int)((map.length-1)*SCALE*RESOLUTION);
		final int ySize = (int)((map[0].length-1)*SCALE*RESOLUTION);
		JFrame dfFrame = new JFrame("Map");
		
		final JPanel panel = new JPanel() {
			/**
			 * 
			 */
			private static final long serialVersionUID = -9195863914868594326L;

			private void paintMap(Graphics2D g) {
				g.setStroke(new BasicStroke(1.0f));
				g.setColor(Color.black);
				for (int i = 0; i < xSize; i++) {
					for (int j = 0; j < ySize; j++) {
						Point p = toMap(i,j);
						if (isObstacle(p)) {
							g.fillRect(i,j,1,1);
						}
					}
				}
			}

			private void paintGrid(Graphics2D g) {
				g.setStroke(new BasicStroke(1.0f));
				g.setColor(new Color(0.7f,0.7f,0.7f));
				double interval = 1;
				int counter = 0;
				while(counter*interval < (map.length-1)*SCALE) {
					Point p1 = toScreen(counter*interval,0);
					Point p2 = toScreen(counter*interval,(map[0].length-1)*SCALE);
					g.drawLine(p1.x(), p1.y(), p2.x(), p2.y());					
					counter++;
				}
				counter = 0;
				while(counter*interval < (map[0].length-1)*SCALE) {
					Point p1 = toScreen(0,counter*interval);
					Point p2 = toScreen((map.length-1)*SCALE,counter*interval);
					g.drawLine(p1.x(), p1.y(), p2.x(), p2.y());				
					counter++;
				}
			}
			
			public void paintDecorations(Graphics2D g) {
				g.setStroke(new BasicStroke(1.0f));
				g.setColor(Color.green);
				for (Point p : decorationPoints) {
					if (p != null) {
						Point screen = toScreen(p.x, p.y);
						g.fillOval(screen.x()-4, screen.y()-4, 8, 8);
					}
				}
			}
						
			public void paintComponent (Graphics g) {
				super.paintComponent(g);
				Graphics2D g2 = (Graphics2D)g;
				//draw the map
				paintMap(g2);
				//paint a grid on top of map 
				paintGrid(g2);
				//Draw polygons obtained from given points (clockwise order)
				for (int i = 0; i < cps.length; i++) cps[i].draw(g2, cpNames[i]);
				//for (ConvexSpatialPolygon cp : cps) cp.draw(g2);
				//Draw paths
				for (Path p : paths.values()) p.draw(g2);
				//draw extra decorations, if any
				if (decorationPoints != null) paintDecorations(g2);
			}
		};
		
		dfFrame.setLayout(new BoxLayout(dfFrame.getContentPane(), BoxLayout.Y_AXIS));
		
		panel.setPreferredSize(new Dimension(xSize,ySize));
		JScrollPane scrollPane = new JScrollPane(panel);
		
		animateButton.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				animateButton.setEnabled(false);
				animate();
			}
		});
				
		solveButton.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				solveButton.setEnabled(false);
				boolean solved = solve();
				System.out.println("Solved? " + solved);
				if (solved) {
					animateButton.setEnabled(true);
					resetButton.setEnabled(true);
				}
				solveButton.setEnabled(true);
				//save current image to file
				if (writeImage) {
					BufferedImage bi = new BufferedImage(xSize, ySize, BufferedImage.TYPE_INT_ARGB);
				    Graphics2D ig2 = bi.createGraphics();
				    myPanel.paint(ig2);
					try { ImageIO.write(bi, "PNG", new File("dist/SAUNA/benchmarks/image.png")); }
					catch (IOException e1) { e1.printStackTrace(); }
				}
				//update GUI image
				myPanel.repaint();
			}
		});

		resetButton.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				reset();
				System.out.println("Solution discarded");
				animateButton.setEnabled(false);
				solveButton.setEnabled(true);
				myPanel.repaint();
			}
		});

		final TemporalPolyhedron[] selected = new TemporalPolyhedron[2];
		final JLabel boundsLabel = new JLabel("Bounds");
		final JTextField bounds1 = new JTextField("null");
		final JTextField bounds2 = new JTextField("null");
//		final JComboBox<AllenIntervalConstraint.Type> constraintTypeList = new JComboBox<AllenIntervalConstraint.Type>(AllenIntervalConstraint.Type.values());
		final JComboBox constraintTypeList = new JComboBox(AllenIntervalConstraint.Type.values());
		constraintTypeList.setSelectedIndex(0);
		//constraintTypeList.addActionListener(this);

		JButton addConstraintButton = new JButton("Add constraint");
		addConstraintButton.addActionListener(new ActionListener() {			
			@Override
			public void actionPerformed(ActionEvent arg0) {
				if (selected[0] != null && selected[1] != null) {
					AllenIntervalConstraint.Type type = (AllenIntervalConstraint.Type)constraintTypeList.getSelectedItem();
					Bounds[] bounds = type.getDefaultBounds();
					if (!(bounds1.getText().equals("null") || bounds2.getText().equals("null"))) {
						String[] lowerBounds = bounds1.getText().split(",");
						String[] upperBounds = bounds2.getText().split(",");
						for (int i = 0; i < bounds.length; i++) {
							bounds[i] = new Bounds(new Double(Double.parseDouble(lowerBounds[i])/Path.TEMPORAL_RESOLUTION).intValue(),new Double(Double.parseDouble(upperBounds[i])/Path.TEMPORAL_RESOLUTION).intValue());
						}
					}
					AllenIntervalConstraint constraint = new AllenIntervalConstraint(type, bounds);
					constraint.setFrom(selected[0]);
					constraint.setTo(selected[1]);
					if (animator == null || animator.isDone()) {
						if (metaCS.getConstraintSolvers()[0].addConstraint(constraint)) {
							selected[0].getSpatialPolyhedron().setColor(Color.blue);
							selected[1].getSpatialPolyhedron().setColor(Color.blue);
							selected[0] = null;
							selected[1] = null;
							System.out.println("Added constraint " + constraint);
							offlineConstraints.add(constraint);
						}
						else {
							System.out.println("Failed to add " + constraint);
						}
					}
					else {
						toAddOnlineConstraints.add(constraint);
					}
					//update GUI image
					myPanel.repaint();
				}
			}
		});

		MouseListener ml = new MouseListener() {
			
			@Override
			public void mouseReleased(MouseEvent arg0) { }
			
			@Override
			public void mousePressed(MouseEvent arg0) { }
			
			@Override
			public void mouseExited(MouseEvent arg0) { }
			
			@Override
			public void mouseEntered(MouseEvent arg0) { }
			
			@Override
			public void mouseClicked(MouseEvent arg0) {
				int x = arg0.getX();
				int y = arg0.getY();
				
				boolean found = false;
				for (TemporalPolyhedron cp : cpsVar) {
					if (cp.getSpatialPolyhedron().contains(new Point(toMap(x,y).x, toMap(x,y).y))) {
						found = true;
						if (arg0.getButton() == MouseEvent.BUTTON1) {
							if (selected[0] != null) {
								selected[0].getSpatialPolyhedron().setColor(Color.blue);
							}
							selected[0] = cp;
							cp.getSpatialPolyhedron().setColor(Color.yellow);
						}
						else if (arg0.getButton() == MouseEvent.BUTTON3) {
							if (selected[1] != null) {
								selected[1].getSpatialPolyhedron().setColor(Color.blue);
							}
							selected[1] = cp;
							cp.getSpatialPolyhedron().setColor(Color.orange);
						}
						break;
					}
				}
				if (!found) {
					if (selected[0] != null) {
						selected[0].getSpatialPolyhedron().setColor(Color.blue);
						selected[0] = null;
					}
					if (selected[1] != null) {
						selected[1].getSpatialPolyhedron().setColor(Color.blue);
						selected[1] = null;
					}
				}
				//update GUI image
				myPanel.repaint();
			}
		};
		
		panel.addMouseListener(ml);
		
		JPanel buttonPane = new JPanel();
		buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS));
		buttonPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10));
		
		buttonPane.add(Box.createHorizontalGlue());
		buttonPane.add(solveButton);

		buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
		buttonPane.add(resetButton);

		buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
		buttonPane.add(animateButton);
		
		buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
		buttonPane.add(constraintTypeList);
		
		buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
		buttonPane.add(boundsLabel);

		buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
		buttonPane.add(bounds1);

		buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
		buttonPane.add(bounds2);
		
		buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
		buttonPane.add(addConstraintButton);

		
		Container contentPane = dfFrame.getContentPane();
		contentPane.add(scrollPane, BorderLayout.CENTER);
		contentPane.add(buttonPane, BorderLayout.PAGE_END);
		
		dfFrame.setPreferredSize(new Dimension(800, 600));
		dfFrame.setResizable(true);
		dfFrame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
		dfFrame.pack();
		dfFrame.setVisible(true);
		myPanel = panel;
	}
	
	public Path getPath(String name) { return paths.get(name); }
	
	public Polyhedron[] makeSpatialEnvelope(Path path, double vLength, double vWidth, double maxFwGrowth, double maxBkGrowth, double maxSideGrowth, double extraGrowth) {
		paths.put(path.getName(), path);
		
		//how much to advance before selecting the next two points
		double distanceThreshold = 1.5;
		double increment = 0.05;
		
		Vector<Polyhedron> polygons = new Vector<Polyhedron>();
		while (path.hasNextPoint()) {
			Point p1 = path.getNextPoint();
			Point p2 = path.getNextPoint();
			//p2 could be null... which means the last point is p1
			if (!path.hasNextPoint()) {
//				System.out.println("ERROR: path has only " + path.getPoints().length + " points");
//				System.out.println(p2);
//				System.exit(0);
				break;
			}
			while (p1.distance(p2) < distanceThreshold) {
				if (path.hasNextPoint())
					p2 = path.getNextPoint();
				else break;
			}
			//front,top,back,bottom
			//this will be the factory method of the MapScheduler
			Polyhedron polygon = new Polyhedron(p1,p2,new double[] {vLength, vWidth},this,maxFwGrowth,maxBkGrowth,maxSideGrowth);
			//ConvexPolygon polygon = new ConvexPolygon(p1,p2,new double[] {dimX, dimY},map);
			polygon.setColor(Color.blue);
			polygons.add(polygon);
			
			//grow more only if:
			// 1. path exits from forward direction
			// 2. exists side of polygon that does not hit obstacle
			Point inside = p2;
			Point outside = null;
			//last polygon, so grow anyway 
			if (!path.hasNextPoint()) {
				while (polygon.grow(increment)) { }
			}
			while (path.hasNextPoint()) {
				outside = path.getNextPoint();
				//System.out.println("D = " + inside.distance(outside));
				if (!polygon.contains(outside)) {
					//System.out.println("[Polygon " + counter + "] Path outside");
					Line l = new Line(inside,outside);
					if (polygon.intersectsForwardConstraint(l, inside)) {
						//System.out.println("[Polygon " + counter + "] Intersection OK");
						boolean grown = false;
						while (grown = polygon.grow(increment)) {
							if (polygon.contains(outside) && path.hasNextPoint()) break;
						}
						if(!grown) {
							//System.out.println("[Polygon " + counter + "] Cannot grow anymore");
							break;							
						}
						else {
							//System.out.println("[Polygon " + counter + "] Grows and OLD OUTSIDE IS NOW INSIDE: " + polygon.contains(outside));
						}
					}
					else {
						//System.out.println("[Polygon " + counter + "] Direction change - stopping growth");
						//polygon.setColor(Color.red);
						double extra = increment;
						while (extra < extraGrowth) {
							if (!polygon.grow(increment)) break;
							extra += increment;
						}
						break;
					}
				}
				else {
					//System.out.println("[Polygon " + counter + "] Path inside");
				}
				inside = outside;
			}
		}
//		System.out.println("Computed " + polygons.size() + " ConvexPolygons for " + path.getName());
		return polygons.toArray(new Polyhedron[polygons.size()]);
	}
	
	public void addDecorations(Point ... points ) {
		this.decorationPoints = points;
	}

	@Override
	public boolean isConflicting(Activity[] peak) {
		if (peak.length == 1) return false;
		for (int i = 0; i < peak.length; i++) {
			TemporalPolyhedron cpi = (TemporalPolyhedron) peak[i];
			for (int j = i+1; j < peak.length; j++) {
				TemporalPolyhedron cpj = (TemporalPolyhedron) peak[j];
				if (cpi.getComponent().equals(cpj.getComponent())) return false;
				if (!cpi.getSpatialPolyhedron().intersects(cpj.getSpatialPolyhedron())) {
					//System.out.println("NOT A PEAK: " + Arrays.toString(peak));
					return false;
				}
			}
		}
		for (Activity cp : peak) ((TemporalPolyhedron)cp).getSpatialPolyhedron().setColor(Color.green);
		//System.out.println("PEAK: " + Arrays.toString(peak));
		//System.out.println("PEAK of size " + peak.length);
		return true;
	}

	public void animate() {
		animateButton.setEnabled(false);
		animator = new MapUpdater(activities.toArray(new Activity[activities.size()]), animateButton, myPanel, toAddOnlineConstraints, onlineConstraints, metaCS.getConstraintSolvers()[0]);
		animator.execute();
	}
	
	public boolean solve() {
		long start = Calendar.getInstance().getTimeInMillis();
		boolean solved = this.metaCS.backtrack();
		System.out.println("Time: " + (Calendar.getInstance().getTimeInMillis()-start));
		System.out.println("Num resolvers:" + this.metaCS.getAddedResolvers().length);
		System.out.println("== RESOLVERS: ==");
		for (ConstraintNetwork cn : this.metaCS.getAddedResolvers()) {
			if (cn.getConstraints() != null) System.out.println(Arrays.toString(cn.getConstraints()));
		}
		System.out.println("================");
		this.metaCS.draw();
		return solved;
	}
		
	public void reset() {
		this.metaCS.retractResolvers();
		for (Variable cp : this.metaCS.getConstraintSolvers()[0].getVariables()) ((TemporalPolyhedron)cp).getSpatialPolyhedron().setColor(Color.blue);
		this.metaCS.getConstraintSolvers()[0].removeConstraints(onlineConstraints.toArray(new Constraint[onlineConstraints.size()]));
		this.metaCS.getConstraintSolvers()[0].removeConstraints(offlineConstraints.toArray(new Constraint[offlineConstraints.size()]));
		this.onlineConstraints = new Vector<AllenIntervalConstraint>();
		this.toAddOnlineConstraints = new Vector<AllenIntervalConstraint>();
		this.offlineConstraints = new Vector<AllenIntervalConstraint>();
	}

	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public String getEdgeLabel() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public Object clone() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public boolean isEquivalent(Constraint c) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public ConstraintSolver getGroundSolver() {
		// TODO Auto-generated method stub
		return null;
	}

		

}
